有时候,我们在登陆页面需要添加验证码,而登陆使用的是Spring Security的登陆流程,就需要我们自定义Spring Security的认证逻辑了。有以下两种方式:
- 添加Spring Security过滤器,这种方式有个弊端,每次请求都会通过该过滤器。但实际上,只需要登录请求经过该过滤器即可,其他请求是不需要经过该过滤器的,存在性能的弊端
1. Authentication接口简析
Spring Security的认证逻辑是在接口AuthenticationProvider
中:
1 | public interface AuthenticationProvider { |
- authenticate方法用来验证用户身份
- supports 则用来判断当前的 AuthenticationProvider 是否支持对应的 Authentication
在 Spring Security 中有一个非常重要的对象叫做 Authentication,我们可以在任何地方注入 Authentication 进而获取到当前登录用户信息,Authentication 本身是一个接口,它实际上对 java.security.Principal 做的进一步封装,我们来看下 Authentication 的定义:
1 | public interface Authentication extends Principal, Serializable { |
Authentication作为一个接口,它有很多的实现类:
- AnonymousAuthenticationToken(匿名登陆)
- UsernamePasswordAuthenticationToken(账户密码登陆)
- RememberMeAuthenticationToken(自动登陆)
每一个 Authentication 都有适合它的 AuthenticationProvider 去处理校验。例如处理 UsernamePasswordAuthenticationToken 的 AuthenticationProvider 是 DaoAuthenticationProvider。
在一次完整的认证中,可能包含多个 AuthenticationProvider,而这多个 AuthenticationProvider 则由 ProviderManager 进行统一管理。
2. DaoAuthenticationProvider简析
当我们使用账户密码登陆时,认证逻辑的处理类是DaoAuthenticationProvider,而DaoAuthenticationProvider继承自
AbstractUserDetailsAuthenticationProvider,我们先来看下它的父类,重点关注authenticate和support方法:
1 | public abstract class AbstractUserDetailsAuthenticationProvider |
由于 AbstractUserDetailsAuthenticationProvider 已经把 authenticate 和 supports 方法实现了(实现了大部分校验工作),所以在 DaoAuthenticationProvider 中,我们主要关注 additionalAuthenticationChecks 方法即可(密码比对工作):
1 |
|
additionalAuthenticationChecks 方法主要用来做密码比对的,逻辑也比较简单,就是调用 PasswordEncoder 的 matches 方法做比对,如果密码不对则直接抛出异常即可。
正常情况下,我们使用用户名/密码登录,最终都会走到这一步。
而 AuthenticationProvider 都是通过 ProviderManager#authenticate 方法来调用的。由于我们的一次认证可能会存在多个 AuthenticationProvider,所以,在 ProviderManager#authenticate 方法中会逐个遍历 AuthenticationProvider,并调用他们的 authenticate 方法做认证,我们来稍微瞅一眼 ProviderManager#authenticate 方法:
1 | public Authentication authenticate(Authentication authentication) |
可以看到,在这个方法中,会遍历所有的 AuthenticationProvider,并调用它的 authenticate 方法进行认证。
3. 自定义认证流程
登录请求是调用 AbstractUserDetailsAuthenticationProvider#authenticate 方法进行认证的,在该方法中,又会调用到 DaoAuthenticationProvider#additionalAuthenticationChecks 方法做进一步的校验,去校验用户登录密码。我们可以自定义一个 AuthenticationProvider 代替 DaoAuthenticationProvider,并重写它里边的 additionalAuthenticationChecks 方法,在重写的过程中,加入验证码的校验逻辑即可。
3.1 验证码生成
导入依赖:
1 | <dependency> |
配置验证码的生成属性:
1 |
|
编写controller,生成验证码图片,并将生成的验证码放入到session中:
1 |
|
最后,记得放行验证码的请求:
1 |
|
浏览器访问/code.jpg就可以获取验证码了。
3.2 提供自定义的ProviderManager
前面我们说,所有的 AuthenticationProvider 都是放在 ProviderManager 中统一管理的,所以接下来我们就要自己提供 ProviderManager。
首先,提供自定义的VerifyCodeAuthenticationProvider:
1 | public class VerifyCodeAuthenticationProvider extends DaoAuthenticationProvider { |
然后注入自定义的 VerifyCodeAuthenticationProvider,这一切操作都在 SecurityConfig 中完成:
1 |
|
创建VerifyCodeAuthenticationProvider,需要提供 UserDetailService 和 PasswordEncoder 实例。